package org.fixies.utils;


import org.apache.bcel.Repository;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LocalVariable;
import org.apache.bcel.classfile.LocalVariableTable;
import org.junit.Test;
import org.junit.runners.Parameterized.Parameters;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

import static org.fixies.utils.StringUtils.concat;
import static org.fixies.utils.StringUtils.join;

/**
 * クラスユーティリティ
 *
 * @author Kawasaki
 */
public final class ClassUtils {

    /** デフォルトコンストラクタ */
    private ClassUtils() {
    }

    /**
     * クラス名よりClassインスタンスを取得する
     *
     * @param clazzName クラス名
     * @return Classインスタンス
     */
    public static Class<?> forName(String clazzName) {
        try {
            return Class.forName(clazzName);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public static String getResourceName(Class<?> clazz) {
        return "/" + clazz.getName().replace('.', '/');
    }

    public static Object newInstance(Class<?> clazz) {
        Validator.notNull(clazz);
        try {
            return clazz.newInstance();
        } catch (InstantiationException e) {
            throw new IllegalArgumentException(concat(
                    "can't instantiate ", clazz.getName(), ".",
                    "this class may indicate interface , abstract, array or primitive. " +
                    "or does not have default constructor."));
        } catch (IllegalAccessException e) {
            throw new UnexpectedProblemException(e);
        }
    }

    /**
     * クラス名よりインスタンスを取得する
     *
     * @param clazzName クラス名
     * @return インスタンス
     */
    public static Object newInstance(String clazzName) {
        return newInstance(forName(clazzName));
    }

    /**
     * クラス名よりインスタンスを取得する
     *
     * @param clazzName クラス名
     * @param args 引数
     * @return インスタンス
     */
    public static Object newInstance(String clazzName, Object[] args) {
        List<Class<?>> argClasses = new ArrayList<Class<?>>(args.length);
        for (Object arg : args) {
            argClasses.add(arg.getClass());
        }

        try {
            Constructor<?> cons = forName(clazzName).getConstructor(
                    argClasses.toArray(new Class<?>[args.length]));
            return cons.newInstance(args);
        } catch (InstantiationException e) {
            throw new UnexpectedProblemException(e, "can't instantiate ", clazzName, ".",
                    "this class may indicate interface , abstract, array or primitive. ");
        } catch (IllegalAccessException e) {
            throw new UnexpectedProblemException(e);
        } catch (SecurityException e) {
            throw new UnexpectedProblemException(e);
        } catch (NoSuchMethodException e) {
            throw new UnexpectedProblemException(e);
        } catch (IllegalArgumentException e) {
            throw new UnexpectedProblemException(e);
        } catch (InvocationTargetException e) {
            throw new UnexpectedProblemException(e);
        }
    }


    /**
     * コールスタックからテストメソッド名を取得する。
     *
     * @param elements スタックトレースエレメント
     * @return テストメソッド名
     */
    public static String getCallingTestMethodName(StackTraceElement[] elements) {
        for (StackTraceElement element : elements) {
            String className = element.getClassName();
            String methodName = element.getMethodName();
            Method m;
            try {
                m = Class.forName(className).getMethod(methodName);
            } catch (NoSuchMethodException e) {
                continue;
            } catch (SecurityException e) {
                throw new UnexpectedProblemException(e);
            } catch (ClassNotFoundException e) {
                throw new UnexpectedProblemException(e);
            }
            if (m.isAnnotationPresent(Test.class)
                    || m.isAnnotationPresent(Parameters.class)) {
                return methodName;
            }
        }
        throw new UnexpectedProblemException("aha?");
    }

    public static Map<String, Class<?>> getMethodParameterInfo(Method method) {
        Class<?>[] types = method.getParameterTypes();
        List<String> names = getMethodParameterNames(method);
        if (types.length != names.size()) {
            throw new IllegalStateException(concat("types=[", join(types) + "] names=[", names, "]"));
        }
        Map<String, Class<?>> result = new LinkedHashMap<String, Class<?>>();
        for (int i = 0; i < types.length; i++) {
            result.put(names.get(i), types[i]);
        }
        return result;
    }
    private static List<String> getMethodParameterNames(Method method) {
        LocalVariable[] vars = getLocalVariable(method).getLocalVariableTable();
        List<String> names = new ArrayList<String>();
        for (LocalVariable var : vars) {
            String name = var.getName();
            if (!name.equals("this")) {
                names.add(name);
            }
        }
        return names;
    }
    
    private static LocalVariableTable getLocalVariable(Method method) {
        JavaClass javaClass =  Repository.lookupClass(method.getDeclaringClass());
        String methodName = method.getName();
        for (org.apache.bcel.classfile.Method m : javaClass.getMethods()) {
            if (m.getName().equals(methodName)) {
                return m.getLocalVariableTable();
            }
        }
        throw new UnexpectedProblemException("nai");
    }
}
